/*
 * FileAsMsg plug-in for Miranda IM and QIP Infium.
 * Sending/receiving files via instant messaging without protocol support.
 * Homepage: http://code.google.com/p/fileasmsg/
 *
 * Copyright (c) 2008, Radist <radist.nt@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Name(s) of contributor(s) may not be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY Radist ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL Radist BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "TransferFile.h"
#include <crtdbg.h>
#include "plugin.h"

struct _allocgr
{
	DWORD g;
	_allocgr()
	{
		SYSTEM_INFO info;
		GetSystemInfo(&info);
		g = info.dwAllocationGranularity;
	}
};

static struct _allocgr granularity;
DWORD TransferFile::def_chunk_size = 128;

TransferFile::TransferFile(void)
{
}

#define HARDCODED_SIZE_LIMIT (8*1024*1024/*8Mb*/)

#pragma warning(disable: 4355)

TransferFile::TransferFile( wchar_t *filename, DWORD flags, DWORD size/*=0*/, DWORD _chunk_size/*=0*/ )
: map_end(0), map_begin(1), mptr(NULL), chunk_size(_chunk_size?_chunk_size:def_chunk_size),
coder(CoderIf::CreateCoderIf(this, flags)), next_chunk(0), _read(0==size), file_size(size), keep_file(true)
{
	if (!_read&&file_size>HARDCODED_SIZE_LIMIT)
	{
		throw LPGENW("File too large");
	}
	hFile = CreateFileW(filename, _read?GENERIC_READ:GENERIC_ALL, FILE_SHARE_READ, NULL, _read?OPEN_EXISTING:CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE==hFile)
	{
		throw LPGENW("Cann't open file");
	}
	if (_read)
	{
		LONG high = 0;
		file_size = SetFilePointer(hFile, 0, &high, FILE_END);
		if (high||file_size>HARDCODED_SIZE_LIMIT)
		{
			throw LPGENW("File too large");
		}
	}
	hMap = CreateFileMappingW(hFile, NULL, _read?PAGE_READONLY:PAGE_READWRITE, 0, file_size, NULL);
	if (!hMap)
	{
		CloseHandle(hFile);
		if (!_read)
		{
			DeleteFile(filename);
		}
		throw LPGENW("Cann't map file");
	}
	file_chunks = (file_size+chunk_size-1)/chunk_size;
	if (!_read)
	{
		completed.resize(file_chunks + 1, false);
	}
	name = _wstrdup(filename);
}

TransferFile::~TransferFile(void)
{
	bool ch = _read||CheckHash();
	if (mptr)
	{
		UnmapViewOfFile((LPCVOID)mptr);
	}
	CloseHandle(hMap);
	CloseHandle(hFile);
	if ((!keep_file&&!_read)||(!ch&&IDNO==MessageBoxW(NULL, TranslateW(L"Yes: leave file as is\nNo: delete file"), TranslateW(L"File checksum error"), MB_ICONERROR|MB_YESNO)))
	{
		DeleteFileW(name);
	}
	_free(name);
}

DWORD TransferFile::AdjustMap( DWORD chunk_no )
{
	DWORD b = chunk_no*chunk_size, e = b + ChunkSize(chunk_no);
	if (b<map_begin||e>=map_end)
	{
		map_begin = b/granularity.g*granularity.g;
		map_end = (e+granularity.g-1)/granularity.g*granularity.g;
		if (map_end>file_size)
		{
			map_end = file_size;
		}
		if (mptr)
		{
			UnmapViewOfFile((LPCVOID)mptr);
		}
		mptr = (unsigned char*)MapViewOfFile(hMap, _read?FILE_MAP_READ:FILE_MAP_READ|FILE_MAP_WRITE, 0, map_begin, map_end-map_begin);
		if (!mptr)
		{
			return 0;
		}
	}
	chunk_begin = b - map_begin;
	return e-b;
}

void TransferFile::RehashFile(void)
{
	Locker lock(cs);
	const DWORD c = granularity.g * 8;
	if (mptr)
	{
		UnmapViewOfFile((LPCVOID)mptr);
	}
	DWORD n = (file_size + c - 1)/c;
	for (DWORD i = 0; i<n; ++i)
	{
		DWORD l = c;
		if (n-1==i)
		{
			l = file_size%c;
			if (!l)
			{
				l = c;
			}
		}
		mptr = (unsigned char*)MapViewOfFile(hMap, FILE_MAP_READ, 0, i*c, l);
		if (!mptr)
		{
			return;
		}
		coder->AddHashPart(mptr, l);
		UnmapViewOfFile((LPCVOID)mptr);
	}
	mptr = NULL;
}

bool TransferFile::CheckHash( void )
{
	if (next_chunk>file_chunks)
	{
		return coder->CheckHash();
	}
	else
	{
		return true;//hash info hasn't been received
	}
}

bool TransferFile::ReadData( char *buf, size_t buflen, DWORD chunk_no )
{
	_ASSERTE(_read);
	_ASSERTE(chunk_no<=file_chunks);
	_ASSERTE(buflen>=12);
#pragma warning(suppress:4996)
	_itoa(chunk_no, buf, CHUNKNO_RADIX);
	size_t l = strlen(buf);
	buf[l] = ':';
	buf += l + 1;
	buflen -= l + 1;
	if (chunk_no<file_chunks)
	{
		Locker lock(cs);
		DWORD len = AdjustMap(chunk_no);
		return coder->Encode(buf, buflen, mptr+chunk_begin, len)+1<buflen;
	}
	//chunk_no==file_chunks
	return 0==strcpy_s(buf, buflen, coder->GetHash());
}

bool TransferFile::WriteData( const char *buf, size_t buflen )
{
	_ASSERTE(!_read);
	DWORD chunk_no = strtoul(buf, (char**)&buf, CHUNKNO_RADIX);
	if ((!chunk_no&&'0'!=buf[-1])||':'!=*buf||!*++buf)
	{
		return false;
	}
	Locker lock(cs);
	if (chunk_no>=file_chunks)
	{
		if (file_chunks==chunk_no)
		{
			coder->NotifyHash(buf);
			completed[file_chunks] = true;
		}
		return true;
	} else if (completed[chunk_no])
	{
		return true;
	}
	DWORD len = AdjustMap(chunk_no);
	return coder->Decode(mptr+chunk_begin, len, buf, chunk_no) ? completed[chunk_no] = true : false;
}

bool TransferFile::GetRepeatList( char *buf, size_t buflen )
{
	_ASSERTE(!_read);
	_ASSERTE(!buf || buflen>=12);
	bool ret = true;
	Locker lock(cs);
	for (DWORD i=0; i<=file_chunks; ++i)
	{
		if (!completed[i])
		{
			ret = false;
			if (buf)
			{
#pragma warning(suppress:4996)
				_itoa(i, buf, CHUNKNO_RADIX);
				size_t len = strlen(buf);
				strcpy_s(buf+len, buflen-len, ",");
				buf+=len+1;
				if ((buflen-=len+1)<12)
				{
					break;
				}
				if (i<file_chunks&&!completed[i+1])
				{
					while (++i<file_chunks&&!completed[i+1]);
					--i;
					buf[-1] = '-';
				}
			}
			else break;
		}
	}
	return ret;
}

bool TransferFile::ReadRepeatChunkIndex( DWORD &index, const char *buf, char **next )
{
	index = strtoul(buf, next, CHUNKNO_RADIX);
	return (index||'0'==*buf)&&(index<=file_chunks);
}

DWORD TransferFile::SetRepeatList( const char *buf )
{
	_ASSERTE(_read);
	DWORD ret = 0;
	char *next = (char*)strchr(buf, ',');
	if (next)
	{
		Locker lock(cs);
		next_chunk = 0;
		completed.resize(0);
		completed.resize(file_chunks + 1, true);
		do 
		{
			DWORD index;
			if (ReadRepeatChunkIndex(index, buf, &next))
			{
				DWORD endindex;
				if ('-'==*next&&*(next+1)&&ReadRepeatChunkIndex(endindex, next+1, &next)&&endindex>index)
				{
					endindex -= index - 1;
					std::fill_n(completed.begin() + index, endindex, false);
					ret += endindex;
				} 
				else
				{
					completed[index] = false;
					++ret;
				}
			}
		} while (*next&&*(buf = next+1));
	}
	return ret;
}

bool TransferFile::GetNext( DWORD &next )
{
	_ASSERTE(_read);
	if (completed.size())
	{
		Locker lock(cs);
		while (next_chunk<file_chunks&&completed[next_chunk])++next_chunk;
	}
	next = next_chunk++;
	return next<=file_chunks;
}

TransferFile * TransferFile::CreateTransferFile( wchar_t *filename, DWORD flags, DWORD file_size /*= 0*/, DWORD chunk_size /*= 0*/ )
{
	return new TransferFile(filename, flags, file_size, chunk_size);
}

